package com.springboot.java_doc_searcher.searcher;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.util.StringUtils;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created with IntelliJ IDEA.
 * Description :
 * User: jiume
 * Date: 2025-06-02
 * Time: 16:12
 */
public class Parser {
    private static final String INPUT_PATH =
            "D:/project/java-doc-searcher/docs/api";

    private static final String ONLINE_PATH =
            "https://docs.oracle.com/javase/8/docs/api";

    private static final Index INDEX = new Index();

    public void run() {
        System.out.println("开始制作索引");
        long begin = System.currentTimeMillis();
        // 1、根据文档路径，枚举出所有的html文件
        List<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH, fileList);
        long enumFileEnd = System.currentTimeMillis();
        System.out.println("遍历文件耗时: "+(enumFileEnd - begin)+" ms");
        // 2、打开文件，读取文件内容，并进行解析，构造索引
        for (File f : fileList) {
            parseHTML(f);
        }
        long parseHTMLend = System.currentTimeMillis();
        System.out.println("解析文件，制作索引耗时: "+(parseHTMLend-enumFileEnd)+" ms");
        // 3、将内存中构造的索引数据结构保存到指定的文件中
        INDEX.save();
        long end = System.currentTimeMillis();
        System.out.println("保存索引耗时: "+(end-parseHTMLend)+" ms");
        System.out.println("索引制作完毕, 耗时: "+(end-begin) + " ms");
    }


    // 解析文件，制作索引的过程在整个程序执行的过程中，耗时占比非常大
    // 因此这里我们使用多线程的方式来处理该任务
    public void runByThread() throws InterruptedException {
        System.out.println("开始制作索引");
        long begin = System.currentTimeMillis();
        // 1、根据文档路径，枚举出所有的html文件
        List<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH, fileList);
        // 2、打开文件，读取文件内容，并进行解析，构造索引
        // 由于该过程任务量比较大，因此采用多线程的方式来处理这些任务
        // 通过参数指定线程池中的线程要完成的任务数
        CountDownLatch latch = new CountDownLatch(fileList.size());
        // 创建带有4个线程的线程池
        ExecutorService service = Executors.newFixedThreadPool(4);
        for (File f : fileList) {
            // 往线程池中添加任务
            service.submit(new Runnable() {
                @Override
                public void run() {
                    parseHTML(f);
                    // 当完成一个任务之后，就统计一次
                    latch.countDown();
                }
            });
        }
        // 3、将内存中构造的索引数据结构保存到指定的文件中
        // 当程序运行到这里时，不代表当前文件的索引全部制作完成，只说明main线程执行到这里了
        // 但是线程池中的线程可能还在执行任务，因此我们得手动等待线程池中的任务执行完毕
        latch.await();
        // 完成任务之后，为了让进程结束，得手动杀死非守护线程
        service.shutdown();
        INDEX.save();
        long end = System.currentTimeMillis();
        System.out.println("索引制作完毕, 耗时: "+(end-begin) + " ms");
    }

    /**
     * 读取HTML文件，解析出最终需要展示的 标题、URL、描述
     * 因为描述是正文的一部分摘要，因此需要先获取正文
     * @param f File文件对象
     */
    private void parseHTML(File f) {
        // 1、获取标题
        String title = parseTitle(f);
        // 2、获取URL
        String url = parseURL(f);
        // 3、获取正文
//        String content = parseContent(f);
//        String content = parseContentByRegex(f);
        String content = parseContentByJsoup(f);
        // 4、构建索引
        INDEX.addDoc(title, url, content);
    }

    // 解析HTML文件获取正文
    public String parseContent(File f){
        if (f == null) {
            System.out.println("[parseContent]: 文件对象为空");
            return null;
        }
        // 遍历文件中的每个字符，提取出正文内容（按照字符读取，使用FileReader）
        // FileReader 的 read() 方法是读取磁盘，效率低下
        // 可以采用 BufferedReader类，其是对 FileReader类 的封装，并自带缓冲区
        // 这里为了增大缓冲区的大小，我们手动设置为 1MB
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(f), 1024*1024);) {
            boolean isCopy = true;
            StringBuilder content = new StringBuilder();
            while (true) {
                // 一次读取一个字符
                int ret = bufferedReader.read();
                // 如果读取到文件末尾的话，最终的返回值结果为-1
                if (ret == -1) { // 说明文件已经读完了，直接退出即可
                    break;
                }
                // 将整型转换为字符
                char c = (char) ret;
                // 判断c的值
                if (c == '<') {
                    // 关闭拷贝
                    isCopy = false;
                } else if (c == '>') {
                    // 开启拷贝
                    isCopy = true;
                } else {
                    // 如果拷贝器是true，则开始拷贝
                    if (isCopy) {
                        // 遇到回车符或者换行符都替换成空格
                        if (c == '\n' || c == '\r') {
                            c = ' ';
                        }
                        content.append(c);
                    }
                }
            }
            return content.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

    // 通过正则表达式来解析正文
    public String parseContentByRegex(File f) {
        // 1、先将文件内容读取到String变量中
        String content = readFile(f);
        // 2、替换掉 script 标签
        content = content.replaceAll("<script.*?>(.*?)</script>", " ");
        // 3、替换掉普通的 html 标签
        content = content.replaceAll("<.*?>", " ");
        // 4、使用正则把多个空格, 合并成一个空格
        content = content.replaceAll("\\s+", " ");
        return content;
    }

    // 读取文件内容，并存放到一个String变量中
    private String readFile(File f) {
        StringBuilder builder = new StringBuilder();
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(f))) {
            // 同样是以字符流来读取文件信息
            while (true) {
                int read = bufferedReader.read();
                if (read == -1) {
                    break;
                }
                char c = (char)read;
                if (c == '\n' || c == '\r') {
                    c = ' ';
                }
                builder.append(c);
            }
            return builder.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

    // 使用 Jsoup 库来处理HTML标签
    public String parseContentByJsoup(File f) {
        // 1、先将文件内容读取到String变量中
        String content = readFile(f);
        // 2、将String变量转换为文档对象
        Document doc = Jsoup.parse(content);
        // 3、删除script标签
        doc.select("script").remove();
        // 4、获取纯文本内容
        return doc.body().text();
    }


    // 解析HTML文件获取URL
    private String parseURL(File f) {
        if (f == null) {
            System.out.println("[parseURL]: 文件对象为空");
            return null;
        }
        // 将HTML文件所处的URL路径截取后半部分 =》丢弃前半部分，也就是文档所处的路径
        // 在加上在线文档的前缀路径，这样就可以构造成在线文档完整URL
        // 虽然这个URL后半部分的分隔符并不是/，而是\，但浏览器的鲁棒性可以识别出来
        // 当然也可以使用 replace方法 将 \ 替换成 /
        String ret = ONLINE_PATH + f.getAbsolutePath().substring(INPUT_PATH.length());
        ret = ret.replace("\\", "/");
        return ret;
    }

    // 解析HTML文件获取标题
    private String parseTitle(File f) {
        if (f == null) {
            System.out.println("[parseTitle]: 文件对象为空");
            return null;
        }
        // 可以打开HTML文件去观察对应的标题就是 title 标签，但更简单的方式是文件名
        // 这里是获取到文件名的总长度减去固定的 .html 字符串的长度，最终得到的就是文件名的长度
        return f.getName().substring(0, f.getName().length() - ".html".length());
    }

    /**
     * 找到当前路径下所有的html文件并将其所在路径保存到列表中
     * @param inputPath 所搜寻的文件路径
     * @param fileList 列表
     */
    private void enumFile(String inputPath, List<File> fileList) {
        // 1、判断参数是否合法
        if (!StringUtils.hasText(inputPath)) {
            System.out.println("[enumFile]: 文件路径为空");
            return;
        }
        if (fileList == null) {
            System.out.println("[enumFile]: 列表对象为空");
            return;
        }
        // 2、遍历当前路径下所有文件
        // 如果是目录的话，递归；如果是普通文件，就判断是否为html文件
        File rootFile = new File(inputPath);
        // 列出当前目录下所有的文件
        File[] files = rootFile.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                // 是目录，继续递归寻找
                enumFile(file.getAbsolutePath(), fileList);
            } else {
                // 是普通文件，判断文件名是否以 .html 结尾
                if (file.getName().endsWith(".html")) {
                    fileList.add(file);
                }
            }
        }
    }

    // 读取文档，并解析文档内容，最终创建索引
    public static void main(String[] args) throws InterruptedException {
        Parser parser = new Parser();
        parser.runByThread();
    }
}
